﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using AdvUtils;
using GeoUtils;
using ListNetRanker;

namespace GeoCoderParser
{
    enum RunMode { NORM_RULE, NORM_LTR };

    public class AddressParser
    {
        WordSeg.WordSeg wordseg;
        Dictionary<int, int> adminCode2PreCode;
        Dictionary<int, string> adminCode2LocName;

        ListNetGeoEntityChainRanker listNetRanker;
        NaiveGeoEntityChainRanker naiveRanker;

        RunMode runMode = RunMode.NORM_LTR;
        string strDataRoot;

        GeoEntityDatabase geoEntityDB;

        private ArrayList[,] gridRegions;
        private const int xCellCount = 361;
        private const int yCellCount = 181;

        public AddressParser()
        {
            listNetRanker = null;
            naiveRanker = null;

        }

        private void GetGridCellIndexByLatlong(LatLong point, out int xCell, out int yCell)
        {
            xCell = (int)point.Longitude + 180;
            yCell = (int)point.Latitude + 90;
        }

        //Mapping geo-entities into lat/long grid
        private void BuildGeoGridFromGeoEntity()
        {
            gridRegions = new ArrayList[xCellCount, yCellCount];

            foreach (List<GeoEntity> itemList in geoEntityDB.GeoEntityList)
            {
                foreach (GeoEntity geoEntity in itemList)
                {
                    if (geoEntity.BoundingPolygon == null ||
                        geoEntity.BoundingRect == null || geoEntity.iLevel > 3)
                    {
                        continue;
                    }

                    // Add this region to all the grid cells that it overlaps.
                    int xCellMin, yCellMin, xCellMax, yCellMax;
                    GetGridCellIndexByLatlong(geoEntity.BoundingRect.SouthWestCorner, out xCellMin, out yCellMin);
                    GetGridCellIndexByLatlong(geoEntity.BoundingRect.NorthEastCorner, out xCellMax, out yCellMax);
                    for (int xCell = xCellMin; xCell <= xCellMax; xCell++)
                    {
                        for (int yCell = yCellMin; yCell <= yCellMax; yCell++)
                        {
                            if (gridRegions[xCell, yCell] == null)
                            {
                                gridRegions[xCell, yCell] = new ArrayList();
                            }
                            gridRegions[xCell, yCell].Add(geoEntity);
                        }
                    }
                }
            }
        }



        public GeoEntity FindRegion(LatLong point)
        {
            int xCell, yCell;
            GetGridCellIndexByLatlong(point, out xCell, out yCell);
            if (xCell >= 0 && xCell < xCellCount &&
                yCell >= 0 && yCell < yCellCount &&
                gridRegions[xCell, yCell] != null)
            {
                foreach (GeoEntity region in gridRegions[xCell, yCell])
                {
                    if (region.ContainsPoint(point))
                    {
                        return region;
                    }
                }
            }
            return null;
           
        }

        //Load admin code ontology from file
        private void LoadAdminCodeRelationship(string strAdminCodeOntologyFileName)
        {
            adminCode2PreCode = new Dictionary<int, int>();
            adminCode2LocName = new Dictionary<int, string>();

            StreamReader sr = new StreamReader(strAdminCodeOntologyFileName);
            while (sr.EndOfStream == false)
            {
                string strLine = sr.ReadLine();
                string[] items = strLine.Split('\t');
                adminCode2PreCode.Add(int.Parse(items[0]), int.Parse(items[1]));
                if (items.Length == 3)
                {
                    adminCode2LocName.Add(int.Parse(items[0]), items[2]);
                }
            }
            sr.Close();
        }


     
        //Initialize the parser
        public bool Initialize(string sdr)
        {
            strDataRoot = sdr;
            if (strDataRoot.EndsWith("\\") == false)
            {
                strDataRoot += "\\";
            }

            //Load word breaker
            wordseg = new WordSeg.WordSeg();

            wordseg.LoadAllDictModel(
                strDataRoot + "location_dm.bin", strDataRoot + "wordner.model", false, null);

            //Load admin code ontology
            LoadAdminCodeRelationship(strDataRoot + "AdminCodeOntology.txt");

            //Load Geo Entity database
            geoEntityDB = new GeoEntityDatabase();
            geoEntityDB.Initialize(strDataRoot);

            //Mapping geo-entities into lat/long grid
            BuildGeoGridFromGeoEntity();

            return true;
        }

        public void SetRunMode(string strRunMode)
        {
            if (strRunMode.ToLower() == "norm_ltr")
            {
                if (listNetRanker == null)
                {
                    listNetRanker = new ListNetGeoEntityChainRanker();
                    runMode = RunMode.NORM_LTR;
                    listNetRanker.Initialize(strDataRoot + "GeoRanker.model");
                }
            }
            else if (strRunMode.ToLower() == "norm_rule")
            {
                runMode = RunMode.NORM_RULE;
                if (naiveRanker == null)
                {
                    naiveRanker = new NaiveGeoEntityChainRanker();
                }
            }
            else
            {
                throw new Exception("Failed: Invalidated run mode.");
            }
        }

        //According ranked location item chain list, fill location information into result
        private void FillGeoChainListToToken(GeoCoderResult geoCoderResult, List<GeoEntityChain> rankedGeoEntityChainList)
        {
            List<Token> tknList = geoCoderResult.tokenList;
            geoCoderResult.geoEntityChainList = new List<GeoEntityChain>();

            int cnt = 0;
            //Iterate each geo-entity chain from high confidence to low confidence
            foreach (GeoEntityChain geoEntityChain in rankedGeoEntityChainList)
            {
                //for each geo-entity chain
                bool bSelectedGeoEntityChain = false;
                if (cnt >= tknList.Count)
                {
                    //no more token need to be filled, exit the loop
                    break;
                }

                //Each geo-entity chain is sorted from bigger place to smaller place
                Token geoEntityParentToken = null;
                foreach (GeoEntity geoEntity in geoEntityChain.geoEntityList)
                {
                    // for each geo-entity item
                    if (cnt >= tknList.Count)
                    {
                        //no more token need to be filled, exit the loop
                        break;
                    }

                    //More than one tokens may be the same location item
                    List<Token> matchedTokenList = new List<Token>();
                    foreach (Token token in tknList)
                    {
                        if (token.candGeoEntityList == null)
                        {
                            //No candidated geo entity assigned to current token, ignored it
                            continue;
                        }

                        if (token.geoEntity != null)
                        {
                            //Has assigned one geo-entity to this token
                            continue;
                        }

                        //This token has not been assigned a geo-entity yet, check its geo-entity candidate list
                        for (int i = 0; i < token.candGeoEntityList.Count; i++)
                        {
                            if (token.candGeoEntityList[i] == geoEntity)
                            {
                                //Found the same geo-entity in the given geo-entity chain
                                matchedTokenList.Add(token);
                                break;
                            }
                        }
                    }

                    foreach (Token token in matchedTokenList)
                    {
                        //Fill geo-entity into the token, and set geo-entity parent/child relationship between tokens
                        bSelectedGeoEntityChain = true;
                        token.geoEntity = geoEntity;
                        token.SuperGeoEntityToken = geoEntityParentToken;
                        if (geoEntityParentToken != null && geoEntityParentToken.geoEntity != token.geoEntity)
                        {
                            geoEntityParentToken.tknSubGeoList.Add(token);
                        }
                        cnt++;
                    }

                    if (matchedTokenList.Count > 0)
                    {
                        geoEntityParentToken = matchedTokenList[0];
                    }

                }

                if (bSelectedGeoEntityChain == true)
                {
                    //Save the reasonable geo-entity chain into the list
                    geoCoderResult.geoEntityChainList.Add(geoEntityChain);
                }
            }

        }


        //Search all possible location chains and rank them
        private List<GeoEntityChain> FindAllGeoEntityChains(List<Token> tknList)
        {
            List<GeoEntityChain> geoEntityChainList = new List<GeoEntityChain>();
            for (int i = 0; i < tknList.Count; i++)
            {
                if (tknList[i].candGeoEntityList == null)
                {
                    continue;
                }

                HashSet<int> setIgnoreIdx = new HashSet<int>();
                setIgnoreIdx.Add(i);

                foreach (GeoEntity geoEntity in tknList[i].candGeoEntityList)
                {
                    GeoEntityChain geoItemChain = new GeoEntityChain();
                    geoItemChain.geoEntityList.Add(geoEntity);
                    geoItemChain.geoTokenList.Add(tknList[i]);
                    FindGeoChains(tknList, setIgnoreIdx, geoItemChain, geoEntityChainList);
                }
            }

            return geoEntityChainList;
        }

        //Check whether iParentAdminCode is the parent of iAdminCode
        private bool IsParent(int iAdminCode, int iParentAdminCode)
        {
            if (adminCode2PreCode.ContainsKey(iAdminCode) == false)
            {
                return false;
            }

            if (adminCode2PreCode[iAdminCode] == iParentAdminCode)
            {
                return true;
            }
            return IsParent(adminCode2PreCode[iAdminCode], iParentAdminCode);
        }


        //Search candidate geo-entity chain recursively
        private void FindGeoChains(List<Token> tknList, HashSet<int> setIgnoreIdx, GeoEntityChain geoEntityChain, List<GeoEntityChain> geoEntityChainList)
        {
            GeoEntity currGeoEntity = geoEntityChain.geoEntityList[geoEntityChain.geoEntityList.Count - 1];
            for (int i = 0; i < tknList.Count; i++)
            {
                if (setIgnoreIdx.Contains(i) == true)
                {
                    //Current token has assigned a geo-entity, ignore it
                    continue;
                }

                if (tknList[i].candGeoEntityList == null)
                {
                    //No canidate geo-entity assigend to current token, ignore it
                    continue;
                }

                //Check each geo-entity whether can be formed a geo-entity chain
                foreach (GeoEntity geoEntity in tknList[i].candGeoEntityList)
                {
                    //Two location should not be equal
                    if (currGeoEntity.IsEqual(geoEntity) == true)
                    {
                        continue;
                    }

                    //Search Condition
                    //1. geo-entity is the parent or grand-parent of current geo-entity
                    //OR
                    //2. two geo-entities have the same admin code
                    if ((IsParent(currGeoEntity.AdminCode, geoEntity.AdminCode) == true && geoEntity.iLevel < currGeoEntity.iLevel) || 
                        (currGeoEntity.AdminCode == geoEntity.AdminCode && geoEntity.iLevel < currGeoEntity.iLevel))
                    {
                        //save current token index in order to ignore it in deeper search
                        setIgnoreIdx.Add(i);

                        geoEntityChain.geoEntityList.Add(geoEntity);
                        geoEntityChain.geoTokenList.Add(tknList[i]);

                        FindGeoChains(tknList, setIgnoreIdx, geoEntityChain, geoEntityChainList);
                        geoEntityChain.geoEntityList.RemoveAt(geoEntityChain.geoEntityList.Count - 1);
                        geoEntityChain.geoTokenList.RemoveAt(geoEntityChain.geoTokenList.Count - 1);
                        setIgnoreIdx.Remove(i);
                    }

                }
            }
            //Sort geo-entity in the chain by level
            GeoEntityChain sortedGeoEntityChain = geoEntityChain.CopyTo();
            sortedGeoEntityChain.Sort();
            //Add the geo-entity chain into list
            geoEntityChainList.Add(sortedGeoEntityChain);
        }

        //Select location items which is in viewport rectangle.
        private List<GeoEntity> SelectGeoEntityInViewPort(List<GeoEntity> geoEntityList, ViewPort viewport)
        {
            List<GeoEntity> rstGeoEntityList = new List<GeoEntity>();
            foreach (GeoEntity geoEntity in geoEntityList)
            {
                if (viewport.IsVisible(geoEntity.LatLong) == true)
                {
                    rstGeoEntityList.Add(geoEntity);
                }
            }

            return rstGeoEntityList;
        }

        //Get zones with high confidence
        private List<int> GetZones(List<Token> tknList)
        {
            List<int> adminList = new List<int>();
            foreach (Token tkn in tknList)
            {
                if (tkn.candGeoEntityList == null)
                {
                    continue;
                }

                foreach (GeoEntity geoEntity in tkn.candGeoEntityList)
                {
                    if (geoEntity.bRewritten == false &&
                        geoEntity.iLevel <= (int)Common.GEO_ENTITY_TYPE.ADMIN3)
                    {
                        adminList.AddRange(geoEntity.AdminCodeList);
                    }
                }

            }

            return adminList;
        }

        //If token's geo-entity is in the zone list, or is the parent of the zone list, save it
        //Otherwise, remove the geo-entity from token's candidate geo-entity list
        private void FilterTokensByZones(List<Token> tknList, List<int> zoneAdminCodeList)
        {
            foreach (Token tkn in tknList)
            {
                if (tkn.candGeoEntityList == null)
                {
                    continue;
                }

                List<GeoEntity> geoEntityList = new List<GeoEntity>();
                foreach (GeoEntity geoEntity in tkn.candGeoEntityList)
                {
                    bool bInZone = false;
                    foreach (int adminCode in geoEntity.AdminCodeList)
                    {
                        foreach (int zoneAdminCode in zoneAdminCodeList)
                        {
                            if (adminCode == zoneAdminCode ||
                                IsParent(adminCode, zoneAdminCode) == true ||
                                IsParent(zoneAdminCode, adminCode) == true)
                            {
                                bInZone = true;
                                break;
                            }
                        }
                        if (bInZone == true)
                        {
                            break;
                        }
                    }

                    if (bInZone == true)
                    {
                        geoEntityList.Add(geoEntity);
                    }
                }

                tkn.candGeoEntityList = null;
                if (geoEntityList.Count > 0)
                {
                    tkn.candGeoEntityList = geoEntityList;
                }
            }
        }


        //Update location candidate list by view port
        private void FilterTokensByViewPort(List<Token> tknList, ViewPort viewport)
        {
            if (viewport == null)
            {
                return;
            }

            foreach (Token tkn in tknList)
            {
                if (tkn.candGeoEntityList == null)
                {
                    continue;
                }

                List<GeoEntity> rstGeoEntityList;
                rstGeoEntityList = SelectGeoEntityInViewPort(tkn.candGeoEntityList, viewport);
                if (rstGeoEntityList.Count > 0)
                {
                    tkn.candGeoEntityList = rstGeoEntityList;
                }
            }
        }

        //Check whether the tag is labled by statistics model
        private bool HasGeoTagByModel(List<string> strTagList)
        {
            foreach (string item in strTagList)
            {
                if (item.StartsWith("LOC") == true)
                {
                    return true;
                }
            }

            return false;
        }


        private List<Token> GetTokenListInSameChain(Token token)
        {
            List<Token> tokenList = new List<Token>();

            //get parents
            if (token.SuperGeoEntityToken != null)
            {
                Token tknSuperGeoEntity = token.SuperGeoEntityToken;
                while (tknSuperGeoEntity != null)
                {
                    tokenList.Add(tknSuperGeoEntity);
                    tknSuperGeoEntity = tknSuperGeoEntity.SuperGeoEntityToken;
                }
            }

            //get children
            Queue<Token> qToken = new Queue<Token>();
            qToken.Enqueue(token);

            while (qToken.Count > 0)
            {
                Token tkn = qToken.Dequeue();
                tokenList.Add(tkn);

                if (tkn.tknSubGeoList.Count > 0)
                {
                    foreach (Token item in tkn.tknSubGeoList)
                    {
                        qToken.Enqueue(item);
                    }
                }
            }

            return tokenList;
        }

        //Remove token's geo-entity reference if the chain is low confidence
        //Low Confidence Chain is that no token is labeled as geo-enbtity by CRF in one chain.
        private void RemoveGeoEntityInLowConfidenceChain(GeoCoderResult geocoderResult)
        {
            List<Token> tokenList = geocoderResult.tokenList;
            if (tokenList.Count == 1)
            {
                //If we only have one token, ignore the process
                return;
            }

            foreach (Token token in tokenList)
            {
                //In a geo-entity chain, one geo-entity should be labeled by statistics model at least
                //Otherwise, this geo-entity will be dropped
                List<Token> tokenListInSameChain = GetTokenListInSameChain(token);
                bool bHasModelTag = false;

                if (tokenListInSameChain.Count == 1 && tokenListInSameChain[0].strTagList.Contains("LOC_ADDR") == true)
                {
                    //If the geo-entity chain only has one token and this token is labled as address line by CRF model, we ignore it.
                    bHasModelTag = false;
                }
                else
                {
                    foreach (Token item in tokenListInSameChain)
                    {
                        if (HasGeoTagByModel(item.strTagList) == true)
                        {
                            bHasModelTag = true;
                            break;
                        }
                    }
                }
                if (bHasModelTag == false)
                {
                    token.geoEntity = null;
                }
            }

            foreach (Token token in tokenList)
            {
                if (token.geoEntity == null)
                {
                    token.SuperGeoEntityToken = null;
                    token.tknSubGeoList.Clear();
                }
            }

            //Remove
            List<GeoEntityChain> geoEntityChainListWithLowQuality = new List<GeoEntityChain>();
            foreach (GeoEntityChain geoEntityChain in geocoderResult.geoEntityChainList)
            {
                for (int i = 0; i < geoEntityChain.geoEntityList.Count; i++)
                {
                    if (geoEntityChain.geoTokenList[i].geoEntity == null)
                    {
                        geoEntityChainListWithLowQuality.Add(geoEntityChain);
                        break;
                    }
                }
            }
            foreach (GeoEntityChain item in geoEntityChainListWithLowQuality)
            {
                geocoderResult.geoEntityChainList.Remove(item);
            }
            
        }

       

        // Parse geography and address information for given string
        public GeoCoderResult Parse(string strQuery, ViewPort viewPort)
        {
            //Word break query without space
            WordSeg.Tokens tokensOfWordBreaker;
            tokensOfWordBreaker = wordseg.CreateTokens(1024);
            wordseg.Segment(strQuery.Replace(" ", ""), tokensOfWordBreaker, true);

            //Create each token and fill all possible canidate location item
             List<Token> tokenList = FillGeoTokenList(tokensOfWordBreaker);

            //Filter token's candidate geo-entites by view port
            //If one or more candidate geo-entities in one token is in current view port, others which is not in view port will be removed.
            //If no candidate geo-entities in a token is in current view port, keep all these geo-entities.
            FilterTokensByViewPort(tokenList, viewPort);

            //Filter token's candidate geo-entities by zone
            FilterTokensByZones(tokenList, GetZones(tokenList));

            //Search all possible location chain
            List<GeoEntityChain> allGeoChainList = FindAllGeoEntityChains(tokenList);

            //rank location chain
            List<GeoEntityChain> rankedGeoChainList;
            if (runMode == RunMode.NORM_LTR)
            {
                rankedGeoChainList = listNetRanker.RankGeoEntityChains(tokenList, allGeoChainList, viewPort);
            }
            else
            {
                rankedGeoChainList = naiveRanker.RankGeoEntityChains(tokenList, allGeoChainList, viewPort);
            }

            GeoCoderResult geoCoderResult = new GeoCoderResult();
            geoCoderResult.adminCode2LocName = adminCode2LocName;
            geoCoderResult.adminCode2PreCode = adminCode2PreCode;
            geoCoderResult.tokenList = tokenList;
            //Save all ranked geo-item chain
            geoCoderResult.allCandGeoEntityChainList = rankedGeoChainList;

            //Fill geo-item chain into token
            //All geo-item chains which is not associated with token will be dropped from result geo-item chain
            FillGeoChainListToToken(geoCoderResult, rankedGeoChainList);

            //Check tokens by given geo-item chains
            RemoveGeoEntityInLowConfidenceChain(geoCoderResult);

            return geoCoderResult;
        }
        
        //Get geo-entity candidates according word broken token list
        private List<Token> FillGeoTokenList(WordSeg.Tokens tokens)
        {
            List<Token> tokenList = new List<Token>();
            for (int i = 0; i < tokens.tokenList.Count; i++)
            {
                Token tk = new Token();
                tk.iOffset = i;
                tk.strTerm = tokens.tokenList[i].strTerm;

                //Parse token's tags
                tk.strTagList = new List<string>();
                foreach (string strTag in tokens.tokenList[i].strTagList)
                {
                    if (strTag.Contains("|") == false)
                    {
                        tk.strTagList.Add(strTag);
                    }
                    else
                    {
                        string[] items = strTag.Split('|');
                        tk.strTagList.AddRange(items);
                    }
                }

                tk.candGeoEntityList = new List<GeoEntity>();
                tk.geoEntity = null;
                tk.tknSubGeoList = new List<Token>();
                tk.SuperGeoEntityToken = null;

                //Try to get current token's candidates geo-entity
                if (HasGeoTagByModel(tk.strTagList) == true)
                {
                    geoEntityDB.GetCandidateSingleGeoEntity(tk);
                    geoEntityDB.GetCandidateCombinedGeoEntity(tk, tokens, i);
                }
                else
                {
                    geoEntityDB.GetCandidateSingleGeoEntity(tk, 5, true);
                }

                tokenList.Add(tk);
            }

            return tokenList;
        }
    }
}
